import random
import inspect
import jsonpickle
#import django.utils.simplejson
#sys.modules['simplejson'] = django.utils.simplejson


class Die():
    """A die with an arbitrary number of side and arbitrary side values

    The default is a six-sided die with side values from 1 to 6.
    
    Usage:

    Die(optional list)
        instantiates Die object with an optional list
        the optional list is the side values for the dice
        the default is a six sided dice numbered from 1 to 6,
        i.e.: [1, 2, 3, 4, 5, 6]

    roll()
        gets a random value from the list (i.e.: rolls the dice!)
        and returns that value

    value()
        returns current value of the die (i.e.: the side that is up)

    sideValues()
        returns the side values as a list (i.e.: the optional list passed
        when instantiating the Die object or the default list of values)
    """

    def __init__(self, sideValues=range(1,7)):
        self.sides = sideValues
        self.value = 0
        self.roll()        #make Die random initially

    def roll(self):
        self.value = self.sides[random.randint(0,len(self.sides)-1)]
        return self.value

    def value(self):
        return self.value

    def __str__(self):
        return str(self.value)

    def __repr__(self):
        return repr(self.value)

    def __len__(self): return len(self.sides)

    def sideValues(self):
        return self.sides

    def setNoState(self):
        self.value = 0



class Yahtzee():
    """A class definition for the classic Yahtzee dice game.
    
    The game consists of five six-sided dice with faces numbering 1 to 6.
    
    More description to come soon!...
    
    """
  
    scores = ['ones', 'twos', 'threes', 'fours', 'fives', 'sixes',
              'three_kind', 'four_kind', 'full_house',
              'small_straight', 'large_straight', 'chance', 'yahtzee']
    
    dice_names = {1:'ones', 2:'twos', 3:'threes', 4:'fours', 5:'fives', 6:'sixes'}

    def __init__(self):
        #self.dice = [Die(), Die(), Die(), Die(), Die()]
        self.dice = [Die() for x in range(5)]
        self.resetGame()

    def resetGame(self):
        #self.yahtzeeFlag = False
        #self.roll_number = 1
        #self.resetRoll()
        self.yahtzeeBonus = 0
        self.currentDiceScore = dict([(x, None) for x in self.scores])

        import copy        
        self.currentGameScore = copy.copy(self.currentDiceScore)
        
        self.resetRoll()

    def resetRoll(self):
        self.keep_dice = ["not_selected_no_action" for x in range(len(self.dice))]
        self.roll_number = 1
        for die in self.dice:
            die.setNoState()
        self.checkScores()

    def diceValues(self):
        return [x.value for x in self.dice]

    def diceState(self):
        #I'm returning a list instead of a tuple so jsonpickle won't encode python tuples with py/tuple (that is, a list can be converted to a json native data type whereas a tuple cannot)
        return [[self.dice[x].value, self.keep_dice[x]] for x in range(len(self.dice))]

    def diceList(self):
        return self.dice

    def getScores(self):
        """Return list of lists containing score category and current dice and game scores. I use a list of lists so jsonpickle will encode the data into to native json data types."""
        return [[x, self.currentDiceScore[x], self.currentGameScore[x]] for x in self.scores]    

    def rollDice(self, whichDie=[]):
        """Rolls dice in self.dice list of Die objects with index positions in whichDie"""
        if len(whichDie) > len(self.dice):
            raise IndexError
        #Do I need to worry about large negative or positive indices?
        #Hmm, it'll raise an IndexError otherwise, so I don't think so...
        #if min(whichDie) < 0 or max(whichDie) > len(self.dice):
        #    raise IndexError
        [self.dice[x] for x in whichDie]    #raise IndexError, TypeError otherwise
        action = ""
        if self.roll_number == 3:
            action = "_no_action"   #don't let user interact with rolls after the third roll!
        self.keep_dice = ["selected"+action for x in range(len(self.dice))]
        for x in whichDie:
            self.dice[x].roll()
            self.keep_dice[x] = "not_selected"+action
        self.checkScores()
        self.roll_number = self.roll_number + 1
    
    def rollAllDice(self):
        """Rolls all your dice!"""
        self.rollDice(range(0,len(self.dice)))  #roll all dice
        #for die in self.dice:
        #    die.roll()
        #
        #self.checkScores()
        #self.roll_number = self.roll_number + 1

    def printScores(self):
        ret = ['Current dice\t\t\tGame Score']
        ret.append('\n'.join(['%s:\t%s\t\t\t%s:\t%s' % (x, self.currentGameScore[x] is not None and '--' or self.currentDiceScore[x], x, self.currentGameScore[x]) for x in self.scores]))
        ret.append(self.printTotals())
        return '\n'.join(ret)
    
    def printTotals(self):
        self.upperScore = sum([self.currentGameScore[x] is not None and self.currentGameScore[x] or 0 for x in ['ones', 'twos', 'threes', 'fours', 'fives', 'sixes']])
        self.upperBonus = self.upperScore >= 63 and 35 or 0
        self.totalScore = self.yahtzeeBonus*100 + \
            self.upperBonus + \
            sum([self.currentGameScore[x] for x in self.currentGameScore if self.currentGameScore[x] is not None])
        return 'Upper score: %d\tUpper bonus: %d\tTotal score: %d\tYahtzee Bonus: %d' % (self.upperScore, self.upperBonus, self.totalScore, self.yahtzeeBonus)
            
    def checkScores(self):
        for k, v in self.currentDiceScore.iteritems():
            try:
                scoreFunction = getattr(Yahtzee, "check_%s" % k)
            except AttributeError:
                print 'check_%s score checking function not found!' % k
            else:
                scoreFunction(self)
            
            #set lower fixed scores when yahtzee has been played and upper score for current yahtzee dice has already been played
        if self.currentDiceScore['yahtzee'] == 50 and self.currentGameScore['yahtzee'] is not None and self.currentGameScore[self.dice_names[self.dice[0].value]]:
            self.currentDiceScore['full_house'] = 25
            self.currentDiceScore['small_straight'] = 30
            self.currentDiceScore['large_straight'] = 40
    
    def score(self, value):
        """sets score for calling function with function name check_KEY
        and key KEY in dictionary self.currentDiceScore where KEY is arbitrary
        (but we have defined KEY names in scores list in the class!)
        
        Usage:
        From within check_KEY method in Yahtzee class,
        call self.score(value) to store the value of KEY in self.currentDiceScore
        dictionary.
        """
        self.currentDiceScore[inspect.stack()[1][3].partition('_')[2]] = value
            
    def check_ones(self):
        self.score(self.diceValues().count(1))
                
    def check_twos(self):
        self.score(self.diceValues().count(2)*2)
    
    def check_threes(self):
        self.score(self.diceValues().count(3)*3)
        
    def check_fours(self):
        self.score(self.diceValues().count(4)*4)
        
    def check_fives(self):
        self.score(self.diceValues().count(5)*5)
        
    def check_sixes(self):
        self.score(self.diceValues().count(6)*6)
        
    def check_three_kind(self):
        self.score(0)
        for x in self.dice[0].sides:
            if self.diceValues().count(x) >= 3:
                self.score(sum(self.diceValues()))
                break
        
    def check_four_kind(self):
        self.score(0)
        for x in self.dice[0].sides:
            if self.diceValues().count(x) >= 4:
                self.score(sum(self.diceValues()))
                break
        
    def check_full_house(self):
        self.score(0)
        for x in self.dice[0].sides:
            if self.diceValues().count(x) == 3:
                for y in self.dice[0].sides:
                    if self.diceValues().count(y) == 2:
                        self.score(25)
                        break   #full house, no need to check other cases!
                break   #3 of a kind, but no full house, so break out!

    def checkMatchingNumbers(self, match):
        return [(set(self.diceValues()) & set(x) == set(x)) for x in match].count(True) > 0
    
    def check_small_straight(self):
        if self.checkMatchingNumbers([[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6]]):
            self.score(30)
        else:
            self.score(0)
        
    def check_large_straight(self):
        if self.checkMatchingNumbers([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]]):
            self.score(40)
        else:
            self.score(0)
        
    def check_chance(self):
        self.score(sum(self.diceValues()))
    
    def check_yahtzee(self):
        self.score(0)
        for x in self.dice[0].sides:
            if self.diceValues().count(x) == 5:
                self.score(50)
                break   #no need to check for other cases because Yahtzee found!   
        
    def StartGame(self):
        self.resetGame()
        while (self.checkGameContinue()):
            self.doGameRound()

        print 'Game Complete! Total scores...'
        print self.printTotals()

    def checkGameContinue(self):        
        """returns true if there are scoring opportunities left"""
        return (self.currentGameScore.values().count(None) > 0)    
    
    def doGameRound(self):
        self.rollDice(range(5))
        print self.doGameScreen()
        
        for i in range(2):
            self.doRoll(i)
            print self.doGameScreen()
        
        self.getScoreCategory()
            
        

    def printDiceInfo(self):
        ret = ['Die\tValue']
        ret.append('---\t-----')
        ret.append('\n'.join(['%d: \t%d' % (x, y) for x,y in enumerate(self.diceValues())]))
        return '\n'.join(ret)
    
    def doGameScreen(self):
        ret = ['']
        ret.append(self.printDiceInfo())
        ret.append('')
        ret.append(self.printScores())
        ret.append('')
        return '\n'.join(ret) 
    
    def doRoll(self, rollNumber):
        while True:
            print 'Rolls left: %d' % (2-rollNumber)
            try:
                a = list(set(self.getDiceToRoll())) #make sure no dupicates
            except:
                print 'Invalid input!'
            else:
                if a == [-1]:
                    print 'Keeping all dice!'
                    break   #break out of While True: loop
                elif a == [-99]:
                    for x in self.dice:
                        x.value = 6 #cheat for yahtzee in sixes
                    
                    #need to check for score, still
                    self.checkScores()
                    break  
                else:
                    if [x in range(5) for x in a].count(False) == 0:
                        print 'Re-rolling %s' % a
                        self.rollDice(a)
                        break #break out of While True: loop
                    else:
                        print 'Invalid number(s)!'
        
    def getDiceToRoll(self):
        diceToRoll = raw_input("Which dice to roll (comma-separted numbers from 0 to 4, -1 for none)? ").split(',')
        if diceToRoll == [-1] or diceToRoll == [-99]:
            return diceToRoll   #-1 is roll no dice, -99 is cheat mode!
        else:
            return [int(x) for x in diceToRoll]
    
    def getScoreCategory(self):
        while True:
            scoreCat = raw_input('Enter which scoring catgory: ')
            try:
                if self.currentGameScore[scoreCat] == None:
                    self.currentGameScore[scoreCat] = self.currentDiceScore[scoreCat]
                    
                    #only ever give bonuses when yahtzee was scored previously
                    if (self.currentGameScore['yahtzee'] == 50) and (scoreCat != 'yahtzee') and (self.currentDiceScore['yahtzee'] == 50):
                        self.yahtzeeBonus += 1  #give yahtzee bonus!
                   
                    break
                else:
                    print 'The %s score category has already been played!' % self.scoreCat
            except:
                print 'Invalid scoring category!'

    def writeScore(self, scoreCat):        
        if self.currentGameScore[scoreCat] == None:
            self.currentGameScore[scoreCat] = self.currentDiceScore[scoreCat]
                    
            #only ever give bonuses when yahtzee was scored previously
            if (self.currentGameScore['yahtzee'] == 50) and (scoreCat != 'yahtzee') and (self.currentDiceScore['yahtzee'] == 50):
                self.yahtzeeBonus += 1  #give yahtzee bonus!
        else:
            raise KeyError

def getNewGameJson():
	return jsonpickle.encode(Yahtzee())            
                    
        
if __name__ == "__main__":
    Y = Yahtzee()
    Y.StartGame()
